package NEHE.JOGL.Port;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
import javax.imageio.ImageIO;
import javax.media.opengl.*;
import javax.media.opengl.glu.GLU;
import javax.swing.*;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.texture.*;
import static javax.media.opengl.GL.*;
import static java.awt.event.KeyEvent.*;

/**
 * NeHe Lesson 10: Loading And Moving Through A 3D World
 */
public class NeheJOGL10World3D extends JPanel implements GLEventListener, KeyListener {
   private static final int REFRESH_FPS = 60;    // Display refresh frames per second
   private GLU glu;             // For the GL Utility
   final FPSAnimator animator;  // Used to drive display() 
   GLCanvas canvas;
   
   // The world
   Sector sector;

   private boolean blendingEnabled; // Blending ON/OFF

   // x and z position of the player, y is 0
   private float posX = 0;
   private float posZ = 0;
   private float headingY = 0; // heading of player, about y-axis
   private float lookUpAngle = 0.0f;

   private float moveIncrement = 0.05f;
   private float turnIncrement = 1.5f; // each turn in degree
   private float lookUpIncrement = 1.0f;

   private float walkBias = 0;
   private float walkBiasAngle = 0;

   private Texture[] textures = new Texture[3];
   private int currTextureFilter = 0; // Which Filter To Use
   private String textureFilename = "images/mud.png";

   // Texture image flips vertically. Shall use TextureCoords class to retrieve the
   // top, bottom, left and right coordinates.
   private float textureTop;
   private float textureBottom;
   private float textureLeft;
   private float textureRight;

   // Constructor
   public NeheJOGL10World3D() {
      canvas = new GLCanvas();
      this.setLayout(new BorderLayout());
      this.add(canvas, BorderLayout.CENTER);
      canvas.addGLEventListener(this);
      canvas.addKeyListener(this);
      canvas.setFocusable(true);  // To receive key event
      canvas.requestFocus();
   
      // Run the animation loop using the fixed-rate Frame-per-second animator,
      // which calls back display() at this fixed-rate (FPS).
      animator = new FPSAnimator(canvas, REFRESH_FPS, true);
   }

   private void setupWorld() throws IOException {
      BufferedReader in = null;
      try {
         in = new BufferedReader(new InputStreamReader(getClass()
               .getResourceAsStream("models/world.txt")));
         String line = null;
         while ((line = in.readLine()) != null) {
            if (line.trim().length() == 0 || line.trim().startsWith("//"))
               continue;
            if (line.startsWith("NUMPOLLIES")) {
               int numTriangles;

               numTriangles = Integer.parseInt(line.substring(line
                     .indexOf("NUMPOLLIES")
                     + "NUMPOLLIES".length() + 1));
               sector = new Sector(numTriangles);
               break;
            }
         }

         for (int i = 0; i < sector.triangles.length; i++) {
            for (int vert = 0; vert < 3; vert++) {
               while ((line = in.readLine()) != null) {
                  if (line.trim().length() == 0 || line.trim().startsWith("//"))
                     continue;
                  break;
               }
               if (line != null) {
                  Scanner scanner = new Scanner(line);
                  sector.triangles[i].vertices[vert].x = scanner.nextFloat();
                  sector.triangles[i].vertices[vert].y = scanner.nextFloat();
                  sector.triangles[i].vertices[vert].z = scanner.nextFloat();
                  sector.triangles[i].vertices[vert].u = scanner.nextFloat();
                  sector.triangles[i].vertices[vert].v = scanner.nextFloat();
                  // System.out.println(sector.triangles[i].vertices[vert]);
               }
            }
         }
      } finally {
         if (in != null)
            in.close();
      }
   }

   // Main program
   public static void main(String[] args) {
      final int WINDOW_WIDTH = 320;
      final int WINDOW_HEIGHT = 240;
      final String WINDOW_TITLE = "Nehe #10: Loading And Moving Through A 3D World";

      JFrame frame = new JFrame();
      final NeheJOGL10World3D joglMain = new NeheJOGL10World3D();
      frame.setContentPane(joglMain);
      frame.addWindowListener(new WindowAdapter() {
         @Override 
         public void windowClosing(WindowEvent e) {
            // Use a dedicate thread to run the stop() to ensure that the
            // animator stops before program exits.
            new Thread() {
               @Override 
               public void run() {
                  joglMain.animator.stop(); // stop the animator loop
                  System.exit(0);
               }
            }.start();
         }
      });
      frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
      frame.setTitle(WINDOW_TITLE);
      frame.setVisible(true);
      joglMain.animator.start(); // start the animation loop
   }

   // ------ Implement methods declared in GLEventListener ------

   /**
    * Called by the drawable immediately after the OpenGL context is initialized for
    * the first time. Can be used to perform one-time OpenGL initialization such as
    * setup of lights and display lists. Run only once.
    */
   @Override
   public void init(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // GL Utilities
      glu = new GLU();
      // Enable smooth shading, which blends colors nicely across a polygon, and
      // smoothes out lighting.
      gl.glShadeModel(GL_SMOOTH);
      // Set background color (in RGBA). Alpha of 0 for total transparency
      gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
      // Setup the depth buffer & enable the depth testing
      gl.glClearDepth(1.0f);
      gl.glEnable(GL_DEPTH_TEST); // enables depth testing
      gl.glDepthFunc(GL_LEQUAL); // the type of depth test to do
      // We want the best perspective correction to be done
      gl.glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

      // read the world
      try {
         setupWorld();
      } catch (IOException e1) {
         e1.printStackTrace();
      }

      // Load the texture image
      try {
         // Use URL so that can read from JAR and disk file.
         BufferedImage image = ImageIO.read(this.getClass().getResource(
               textureFilename));

         // Create a OpenGL Texture object from (URL, mipmap, file suffix)
         textures[0] = TextureIO.newTexture(image, false);
         // Nearest filter is least compute-intensive
         // Use nearer filter if image is larger than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
         // Use nearer filter if image is smaller than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

         // For texture coordinates more than 1, set to wrap mode to GL_REPEAT for
         // both S and T axes (default setting is GL_CLAMP)
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

         textures[1] = TextureIO.newTexture(image, false);
         // Linear filter is more compute-intensive
         // Use linear filter if image is larger than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
         // Use linear filter if image is smaller than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

         textures[2] = TextureIO.newTexture(image, true); // mipmap is true
         // Use mipmap filter is the image is smaller than the texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
               GL_LINEAR_MIPMAP_NEAREST);
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
      } catch (GLException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      // Get the top and bottom coordinates of the textures. Image flips vertically.
      TextureCoords textureCoords;
      textureCoords = textures[0].getImageTexCoords();
      textureTop = textureCoords.top();
      textureBottom = textureCoords.bottom();
      textureLeft = textureCoords.left();
      textureRight = textureCoords.right();

      // Enable the texture
      gl.glEnable(GL_TEXTURE_2D);

      // Blending control
      gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f); // Brightness with alpha
      // Blending function For translucency based On source alpha value
      gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE);
   }

   /**
    * Called by the drawable to initiate OpenGL rendering.
    */
   @Override
   public void display(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // Clear the screen and the depth buffer
      gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      // Reset the view (x, y, z axes back to normal)
      gl.glLoadIdentity();

      // Blending control
      if (blendingEnabled) {
         gl.glEnable(GL_BLEND); // Turn Blending On
         gl.glDisable(GL_DEPTH_TEST); // Turn Depth Testing Off
      } else {
         gl.glDisable(GL_BLEND); // Turn Blending Off
         gl.glEnable(GL_DEPTH_TEST); // Turn Depth Testing On
      }

      // Rotate up and down to look up and down
      gl.glRotatef(lookUpAngle, 1.0f, 0, 0);

      // Player at headingY. Rotate the scene by -headingY instead (add 360 to get a
      // positive angle)
      gl.glRotatef(360.0f - headingY, 0, 1.0f, 0);

      // Player is at (posX, 0, posZ). Translate the scene to (-posX, 0, -posZ)
      // instead.
      gl.glTranslatef(-posX, -walkBias - 0.25f, -posZ);

      // Select a texture based on filter
      textures[currTextureFilter].bind();

      // Process each triangle
      for (int i = 0; i < sector.triangles.length; i++) {
         gl.glBegin(GL_TRIANGLES);
         gl.glNormal3f(0.0f, 0.0f, 1.0f); // Normal pointing out of screen

         // need to flip the image
         float textureHeight = textureTop - textureBottom;
         float u, v;

         u = sector.triangles[i].vertices[0].u;
         v = sector.triangles[i].vertices[0].v * textureHeight - textureBottom;
         gl.glTexCoord2f(u, v);
         gl.glVertex3f(sector.triangles[i].vertices[0].x,
               sector.triangles[i].vertices[0].y, sector.triangles[i].vertices[0].z);

         u = sector.triangles[i].vertices[1].u;
         v = sector.triangles[i].vertices[1].v * textureHeight - textureBottom;
         gl.glTexCoord2f(u, v);
         gl.glVertex3f(sector.triangles[i].vertices[1].x,
               sector.triangles[i].vertices[1].y, sector.triangles[i].vertices[1].z);

         u = sector.triangles[i].vertices[2].u;
         v = sector.triangles[i].vertices[2].v * textureHeight - textureBottom;
         gl.glTexCoord2f(u, v);
         gl.glVertex3f(sector.triangles[i].vertices[2].x,
               sector.triangles[i].vertices[2].y, sector.triangles[i].vertices[2].z);

         gl.glEnd();
      }
   }

   /**
    * Called by the drawable during the first repaint after the component has been
    * resized. Run at least once when the window is first set to visible.
    */
   @Override
   public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();

      if (height == 0) {
         height = 1; // prevent divide by zero
      }
      float aspect = (float)width / height;

      // Set the viewport (display area) to cover the entire window
      gl.glViewport(0, 0, width, height);

      // Enable perspective view (where far objects are smaller)
      gl.glMatrixMode(GL_PROJECTION);
      gl.glLoadIdentity(); // reset
      // View angle of 45 degrees, based on this canvas's aspect ratio. The 0.1f,
      // 100.0f is the starting and ending points for how deep we can draw into
      // the screen.
      glu.gluPerspective(45.0f, aspect, 0.1f, 100.0f); // fovy, aspect, zNear, zFar

      // Enable the model view - any new transformations will affect the model-view
      // matrix
      gl.glMatrixMode(GL_MODELVIEW);
      gl.glLoadIdentity(); // reset
   }

   /**
    * Called when the display mode (eg. resolution) has been changed.
    */
   @Override
   public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
         boolean deviceChanged) {}

   // ----- Implement methods declared in KeyListener -----

   @Override
   public void keyPressed(KeyEvent e) {
      switch (e.getKeyCode()) {
         case VK_LEFT: // player turns left (scene rotates right)
            headingY += turnIncrement;
            break;
         case VK_RIGHT: // player turns right (scene rotates left)
            headingY -= turnIncrement;
            break;
         case VK_UP:
            // Player move in, posX and posZ become smaller
            posX -= (float)Math.sin(Math.toRadians(headingY)) * moveIncrement;
            posZ -= (float)Math.cos(Math.toRadians(headingY)) * moveIncrement;

            walkBiasAngle = (walkBiasAngle >= 359.0f) ? 0.0f : walkBiasAngle + 10.0f;
            // What is this walkbias? It's a word I invented :-) It's basically an
            // offset that occurs when a person walks around (head bobbing up and
            // down like a buoy. It simply adjusts the camera's Y position with a
            // sine wave. I had to put this in, as simply moving forwards and
            // backwards didn't look to great.

            // Causes the player to bounce in sine-wave pattern rather than
            // straight-line
            walkBias = (float)Math.sin(Math.toRadians(walkBiasAngle)) / 20.0f;
            break;
         case VK_DOWN:
            // Player move out, posX and posZ become bigger
            posX += (float)Math.sin(Math.toRadians(headingY)) * moveIncrement;
            posZ += (float)Math.cos(Math.toRadians(headingY)) * moveIncrement;
            walkBiasAngle = (walkBiasAngle <= 1.0f) ? 359.0f : walkBiasAngle - 10.0f;
            walkBias = (float)Math.sin(Math.toRadians(walkBiasAngle)) / 20.0f;
            break;
         case KeyEvent.VK_PAGE_UP:
            // player looks up, scene rotates in negative x-axis
            lookUpAngle -= lookUpIncrement;
            break;
         case KeyEvent.VK_PAGE_DOWN:
            // player looks down, scene rotates in positive x-axis
            lookUpAngle += lookUpIncrement;
            break;
         case VK_T: // switch texture filter nearer -> linear -> mipmap
            currTextureFilter = (currTextureFilter + 1) % textures.length;
            break;
         case VK_B: // toggle blending mode
            blendingEnabled = !blendingEnabled;
            break;
      }
   }

   @Override
   public void keyReleased(KeyEvent e) {}

   @Override
   public void keyTyped(KeyEvent e) {
      switch (e.getKeyChar()) {

      }
   }

   // A sector comprises many triangles
   class Sector {
      Triangle[] triangles;

      // Constructor
      public Sector(int numTriangles) {
         triangles = new Triangle[numTriangles];
         for (int i = 0; i < numTriangles; i++) {
            triangles[i] = new Triangle();
         }
      }
   }

   // A triangle has 3 vertices
   class Triangle {
      Vertex[] vertices = new Vertex[3];

      public Triangle() {
         vertices[0] = new Vertex();
         vertices[1] = new Vertex();
         vertices[2] = new Vertex();
      }
   }

   // A vertex has xyz (location) and uv (for texture)
   class Vertex {
      float x, y, z; // 3D x,y,z location
      float u, v; // 2D texture coordinates

      public String toString() {
         return "(" + x + "," + y + "," + z + ")" + "(" + u + "," + v + ")";
      }
   }
}
